/**
 * 
 */
package com.tiduyun.data.exchange.service.impl;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import javax.annotation.PostConstruct;

import org.apache.commons.collections4.MapUtils;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.apache.commons.lang3.time.DateUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.util.ResourceUtils;

import com.alipay.api.internal.util.AlipayEncrypt;
import com.alipay.api.internal.util.AlipaySignature;
import com.tiduyun.data.exchange.constant.ExceptionCode;
import com.tiduyun.data.exchange.exception.CodedRuntimeException;
import com.tiduyun.data.exchange.model.GxVehicleDataExchangeResult;
import com.tiduyun.data.exchange.service.GxVehicleDataAdapterService;
import com.tiduyun.data.exchange.service.GxVehicleDataExchangeGatewayService;
import com.tiduyun.data.exchange.util.JSONUtils;
import com.tiduyun.data.exchange.util.ParamCheckUtils;

import lombok.extern.slf4j.Slf4j;

/**
 * @author wuzheng@tiduyun.com
 *
 */
@Service("gxVehicleDataExchangeGatewayServiceImpl")
@Slf4j
public class GxVehicleDataExchangeGatewayServiceImpl implements GxVehicleDataExchangeGatewayService {

    private static Object[] ALLOW_SIGN_TYPES = new Object[] { "RSA2" };

    private static final String KEY_FIELD_RSA2_PUBLIC_KEY = "rsa2PublicKey";

    private static final String KEY_FIELD_RSA2_PRIVATE_KEY = "rsa2PrivateKey";

    private static final String KEY_FIELD_AES_KEY = "aesKey";

    interface OpenApiRequestHandler {
        Map<String, Object> handle(Map<String, Object> params);
    }

    @Autowired
    @Qualifier("gxVehicleDataAdapterServiceImpl")
    private GxVehicleDataAdapterService gxVehicleDataAdapterService;

    @Autowired
    @Qualifier("mockGxVehicleDataAdapterServiceImpl")
    private GxVehicleDataAdapterService mockGxVehicleDataAdapterService;

    private Map<String, Map<String, OpenApiRequestHandler>> versionedHandleByMethod;

    private Map<String, Map<String, String>> keyByAppId;

    @Value("${td-data.gx-vehicle.key-file-path}")
    private String keyFilePath;

    @Value("${td-data.gx-vehicle.debug.mock.enable}")
    private boolean mockEnable = false;

    private boolean ignoreCommonCheckError = false;

    @SuppressWarnings("unchecked")
    @PostConstruct
    public void initService() throws IOException {
        File keyFile = ResourceUtils.getFile(keyFilePath);
        String keyContent = FileUtils.readFileToString(keyFile, "UTF-8");
        Map<String, Object> keyJson = JSONUtils.parseMap(keyContent);

        keyByAppId = new HashMap<>();
        for (Map.Entry<String, Object> entry : keyJson.entrySet()) {
            Map<String, Object> keyMap = (Map<String, Object>) entry.getValue();

            Map<String, String> key = new HashMap<>();
            key.put(KEY_FIELD_RSA2_PUBLIC_KEY, (String) keyMap.get(KEY_FIELD_RSA2_PUBLIC_KEY));
            key.put(KEY_FIELD_RSA2_PRIVATE_KEY, (String) keyMap.get(KEY_FIELD_RSA2_PRIVATE_KEY));
            key.put(KEY_FIELD_AES_KEY, (String) keyMap.get(KEY_FIELD_AES_KEY));
            keyByAppId.put(entry.getKey(), key);
        }

        if (mockEnable) {
            log.warn("Mock gxVehicleDataAdapterService enabled!");
            gxVehicleDataAdapterService = mockGxVehicleDataAdapterService;
        }

        versionedHandleByMethod = new HashMap<>();

        Map<String, OpenApiRequestHandler> getVehicleInfoHandlers = new HashMap<>();
        getVehicleInfoHandlers.put("0.0", new OpenApiRequestHandler() {

            @Override
            public Map<String, Object> handle(Map<String, Object> params) {
                return mockGxVehicleDataAdapterService.getVehicleInfo(params);
            }

        });
        getVehicleInfoHandlers.put("1.0", new OpenApiRequestHandler() {

            @Override
            public Map<String, Object> handle(Map<String, Object> params) {
                return gxVehicleDataAdapterService.getVehicleInfo(params);
            }

        });
        versionedHandleByMethod.put("spi.alipay.eco.mycar.vehownerprod.vehicle.query", getVehicleInfoHandlers);

        Map<String, OpenApiRequestHandler> checkVehicleInvoiceHandlers = new HashMap<>();
        checkVehicleInvoiceHandlers.put("0.0", new OpenApiRequestHandler() {

            @Override
            public Map<String, Object> handle(Map<String, Object> params) {
                return mockGxVehicleDataAdapterService.checkVehicleInvoice(params);
            }

        });
        checkVehicleInvoiceHandlers.put("1.0", new OpenApiRequestHandler() {

            @Override
            public Map<String, Object> handle(Map<String, Object> params) {
                return gxVehicleDataAdapterService.checkVehicleInvoice(params);
            }

        });
        versionedHandleByMethod.put("spi.alipay.eco.mycar.vehownerprod.invoice.match", checkVehicleInvoiceHandlers);
    }

    /**
     * @see com.tiduyun.data.exchange.service.GxVehicleDataExchangeGatewayService#execute(java.util.Map,
     *      java.lang.String)
     */
    @Override
    public GxVehicleDataExchangeResult execute(Map<String, Object> queryParams, String body) {
        log.info("Execute: queryParams={}, body={}", queryParams, body);

        Object result = null;
        try {
            checkParams(queryParams, body);
            Map<String, Object> bizParams = parseBizParams(queryParams, body);
            checkSign(queryParams, bizParams, body);

            String method = getRequiredStringParam(queryParams, "method");
            String version = getRequiredStringParam(queryParams, "version");
            result = handle(method, version, bizParams);
        } catch (Exception e) {
            Map<String, String> errorResult = new HashMap<>();
            if (e instanceof CodedRuntimeException) {
                CodedRuntimeException cre = (CodedRuntimeException) e;
                errorResult.put("code", cre.getCode());
                errorResult.put("msg", cre.getMessage());

                log.warn("Execute failed: code={}, msg={}", cre.getCode(), cre.getMessage());
            } else {
                errorResult.put("code", ExceptionCode.SYSTEM_ERROR);
                errorResult.put("msg", "system error");

                log.warn("Execute failed", e);
            }
            result = errorResult;
        }
        return wrapResult(queryParams, result);
    }

    protected void checkParams(Map<String, Object> queryParams, String body) {
        getRequiredStringParam(queryParams, "method");
        getRequiredStringParam(queryParams, "version");
        getRequiredStringParam(queryParams, "charset");
        String utcTimestamp = getRequiredStringParam(queryParams, "utc_timestamp");
        String signType = getRequiredStringParam(queryParams, "sign_type");
        ParamCheckUtils.valueAllow(ALLOW_SIGN_TYPES, signType, "sign_type");
        getRequiredStringParam(queryParams, "sign");

        // check timestamp
        long nowTimestamp = System.currentTimeMillis();
        long allowTimestampError = 600000; // 允许10分钟的时间戳误差
        long inputTimestamp = 0;
        try {
            inputTimestamp = DateUtils.parseDate(utcTimestamp, "yyyy-MM-dd HH:mm:ss").getTime() + 3600000 * 8;
        } catch (Exception e) {
            log.warn("Parse utc timestamp failed: utcTimestamp={}", utcTimestamp, e);
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "invalid timestamp");
        }
        if (Math.abs(inputTimestamp - nowTimestamp) > allowTimestampError) {
            log.warn("Check utc timestamp failed: nowTimestamp={}, inputTimestamp={}", nowTimestamp, inputTimestamp);
            if (!ignoreCommonCheckError) {
                throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "invalid timestamp");
            }
        }
    }

    protected String getRequiredStringParam(Map<String, Object> queryParams, String paramName) {
        String value = (String) queryParams.get(paramName);
        ParamCheckUtils.notEmptyString(value, paramName);
        return value;
    }

    protected void checkSign(Map<String, Object> queryParams, Map<String, Object> bizParams, String body) {
        String appId = (String) queryParams.get("biz_app_id");
        String charset = (String) queryParams.get("charset");
        String publicKey = getKey(appId, KEY_FIELD_RSA2_PUBLIC_KEY);
        Map<String, String> signParams = new HashMap<>();
        putAll(signParams, queryParams);
        putAll(signParams, bizParams);
        boolean isPass = false;
        try {
            log.info("Check sign: signParams={}", signParams);
            isPass = AlipaySignature.rsaCheckV1(signParams, publicKey, charset, "RSA2");
        } catch (Exception e) {
            log.error("Check sign failed", e);
            isPass = false;
        }

        if (!isPass && !ignoreCommonCheckError) {
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "invalid sign");
        }
    }

    private void putAll(Map<String, String> valueMap, Map<String, Object> inputMap) {
        for (Map.Entry<String, Object> entry : inputMap.entrySet()) {
            valueMap.put(entry.getKey(), Objects.toString(entry.getValue(), null));
        }
    }

    protected Map<String, Object> parseBizParams(Map<String, Object> queryParams, String body) {
        String appId = (String) queryParams.get("biz_app_id");
        String charset = (String) queryParams.get("charset");
        String json = decryptParams(body, appId, charset);
        try {
            return JSONUtils.parseMap(json);
        } catch (Exception e) {
            log.error("Parse params failed: json={}", json, e);
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "invalid body params");
        }
    }

    protected Object handle(String method, String version, Map<String, Object> bizParams) {
        log.info("Handle: method={}, version={}, bizParams={}", method, version, bizParams);

        Map<String, OpenApiRequestHandler> handlerByVersion = versionedHandleByMethod.get(method);
        if (MapUtils.isEmpty(handlerByVersion)) {
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "unsupported method");
        }
        OpenApiRequestHandler handler = handlerByVersion.get(version);
        if (handler == null) {
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "unsupported version");
        }
        Map<String, Object> result = handler.handle(bizParams);
        if (result == null) {
            result = new HashMap<>();
        }
        if (!result.containsKey("code")) {
            result.put("code", "10000");
            result.put("code", "Success");
        }

        log.info("Handle result: result={}", result);
        return result;
    }

    protected GxVehicleDataExchangeResult wrapResult(Map<String, Object> queryParams, Object result) {
        String appId = (String) queryParams.get("biz_app_id");
        String charset = (String) queryParams.get("charset");

        String response = formatResponse(result);
        try {
            response = encryptResponse(response, appId, charset);
        } catch (Exception e) {
            log.error("Encrypt response failed: appId={}, charset={}", appId, charset, e);
        }

        String utcTimestamp = DateFormatUtils.formatUTC(System.currentTimeMillis(), "yyyy-MM-dd HH:mm:ss");

        String sign = null;
        try {
            sign = createResponseSign(response, utcTimestamp, appId, charset);
        } catch (Exception e) {
            log.error("Create response sign failed: appId={}, charset={}", appId, charset, e);
        }

        GxVehicleDataExchangeResult r = new GxVehicleDataExchangeResult();
        r.setUtcTimestamp(utcTimestamp);
        r.setResponse(response);
        r.setSign(sign);

        log.info("Wrapped result: result={}", r);

        return r;
    }

    private String formatResponse(Object result) {
        if (result == null) {
            return null;
        }
        if (result instanceof String) {
            return (String) result;
        }
        return JSONUtils.toJSONStringIgnoreErrors(result);
    }

    protected String decryptParams(String content, String appId, String charset) {
        String encryptKey = getKey(appId, KEY_FIELD_AES_KEY);
        try {
            return AlipayEncrypt.decryptContent(content, "AES", encryptKey, charset);
        } catch (Exception e) {
            log.error("Decrypt params failed: content={}, appId={}, charset={}", new Object[] { content, appId, charset, e });
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "invalid body params");
        }
    }

    protected String encryptResponse(String content, String appId, String charset) {
        log.info("Encrypt reponse: content={}, appId={}, charset={}", content, appId, charset);
        String encryptKey = getKey(appId, KEY_FIELD_AES_KEY);
        try {
            return AlipayEncrypt.encryptContent(content, "AES", encryptKey, charset);
        } catch (Exception e) {
            log.error("Encrypt response failed: content={}, appId={}, charset={}", new Object[] { content, appId, charset, e });
            throw new CodedRuntimeException(ExceptionCode.SYSTEM_ERROR, "response failed");
        }
    }

    protected String createResponseSign(String content, String utcTimestamp, String appId, String charset) throws Exception {
        String encryptKey = getKey(appId, KEY_FIELD_RSA2_PRIVATE_KEY);
        String willSignStr = StringUtils.join("response=", content, "&utc_timestamp=", utcTimestamp);
        log.info("Create response sign: willSignStr={}", willSignStr);
        return AlipaySignature.rsaSign(willSignStr, encryptKey, charset, "RSA2");
    }

    protected String getKey(String appId, String keyName) {
        if (StringUtils.isEmpty(appId)) {
            appId = "alipay-gx-vehicle";
        }

        String key = null;
        Map<String, String> keyMap = keyByAppId.get(appId);
        if (MapUtils.isNotEmpty(keyMap)) {
            key = keyMap.get(keyName);
        }
        if (key == null) {
            throw new CodedRuntimeException(ExceptionCode.INVALID_PARAMETER, "invalid biz_app_id");
        }
        return key;
    }

    public boolean isIgnoreCommonCheckError() {
        return ignoreCommonCheckError;
    }

    public void setIgnoreCommonCheckError(boolean ignoreCommonCheckError) {
        this.ignoreCommonCheckError = ignoreCommonCheckError;
    }

}
